Reporte de resultados y productos de datos

Una parte de los proyectos de ciencia de datos que suele no ser estudiada en profundidad es la de generar productos de datos o reportes de resultados. Una vez que terminamos de analizar nuestros datos ¿Cómo podemos mostrar nuestos resultados? Todavía más ¿Cómo podemos explorar interactivamente nuestros datos para saber si hay otros análisis que podemos hacer?

En esta nota de clase vamos a introducirnos a Shiny un framework que nos permite crear aplicaciones interactivas basadas en R. En principio, no tenemos que aprender nada más que R para desarrollar nuestras aplicaciones y permitir que otros puedan acceder a ellas.

Organizando nuestra aplicación

Comencemos nuestra introducción a Shiny a través de la organización de su desarrollo dentro de un proyecto en RStudio. Hasta ahora, siempre que creamos un proyecto desde RStudio elegimos la opción R Project, ahora vamos a hacer algo un poco distinto. Vayan a File -> New Project -> Shiny Web Application y elijan un lugar donde quieran crear ese proyecto.

Van a observar que lo que hace RStudio es un poco distinto a lo que hace cuando creamos un proyecto de R. La principal diferencia es que aparece un archivo con nombre app.R que contiene un ejemplo mínimo de cómo funciona una shiny app. Para ver todo su potencial, simplemente seleccionen todo el código de app.R (pueden usar el atajo Ctrl+A) y ejecutenlo. Debería aparecerles una nueva ventana de R con una aplicación de shiny en la cual pueden modificar la cantidad de “bins” o categorías para discretizar los datos continuos del famoso dataset de “Old Faithful”. Prueben modificando los valores, a ver qué sucede. Por otro lado, pueden visualizar esta aplicación directamente en un navegador haciendo click en “Open in Browser”.

Todo esto esta resumido en el siguiente gif

Creando un proyecto para una aplicación Shiny desde RStudio

¿Qué es lo que hace shiny? Pueden verlo ustedes mismos modificando el elemento que tienen en el panel izquierdo de la pantalla. Ese control determina la cantidad de categorías (bins, en inglés) que tendrá el gráfico que se encuentra en el panel derecho. Con esta breve demostración podemos ver que lo que hace shiny es responder a cambios en valores que recibe desde distintos inputs, este es el principio de reactividad. Como regla general, nosotros tenemos un conjunto de controles que los usuarios pueden modificar y también un conjunto de gráficos, tablas o simplemente texto donde vamos a mostrar información que va a cambiar cada vez que el usuario modifique alguno de los controles que le ponemos a disposición. La relación entre lo que el usuario elige y lo que la aplicación muestra está mediado por operaciones que nosotros específicamos. Y lo mejor de todo: ¡No tenemos que usar otra cosa que R!

Como regla general para desarrollar sus primeras aplicaciones, siempre que trabajen desarrollando aplicaciones de shiny es muy conveniente contar con un archivo app.R donde tengan tres componentes claves: ui, server y la llamada para que se ejecute la shinyapp con la función shinyApp()**

Estructura de una shiny app

Como se adelantó brevemente en la sección anterior, nuestros archivos .R conteniendo una aplicación shiny tienen tres componentes claves:

Analicemos con un poco de detalle cuál es la sintáxis para cada uno de estos elementos en la pequeña aplicación de demostración que nos muestra RStudio

ui <- fluidPage(

    # Título para la aplicación
    titlePanel("Old Faithful Geyser Data"),

    # En la barra del costado va a crear un sliderInput() 
    sidebarLayout(
        sidebarPanel(
            sliderInput(inputId = "bins",
                        label = "Number of bins:",
                        min = 1,
                        max = 50,
                        value = 30)
        ),

        # En el panel dentral va a mostrar un gráfico
        mainPanel(
           plotOutput(outputId = "distPlot")
        )
    )
)

La función fluidPage() genera un diseño muy simple de HTML en el cual hay un título en la parte superior de la página, una “sidebar”, es decir una barra que se encuentra a los costados, y un panel principal, dentro de mainpanel().

Dentro del sidebarPanel() van a ver que se crea un sliderInput(), que es tan solo uno de los elementos qe shiny brinda para que nuestros usuarios interactuen con nuestros datos. El primer parámetro es inputId, muy importante para luego poder usar el valor que el usuario elija en nuestro gráfico. label es simplemente es un texto que va a ver el usuario, sirve como referencia sobre qué va a hacer. Luego, min y max marcan el valor mínimo y máximo que queremos que los usuarios puedan modificar, mientras que value simplemente marca el valor con el que arranca por primera vez nuestra aplicación.

Dentro del mainPanel() en esta aplicación se muestra solo un gráfico, creado con plotOutput(). En este caso, solo estamos obligados a pasarle un nombre a este objeto, que se llamará “distPlot”.

Fijense que tanto los input como los output llevan nombres. Esto no es casualidad, va a ser la forma mediante la cual vamos a poder interactuar con ellos desde el server. Veamos está parte en el código simple de muestra:

server <- function(input, output) {

    output$distPlot <- renderPlot({
        # generate bins based on input$bins from ui.R
        x    <- faithful[, 2]
        bins <- seq(min(x), max(x), length.out = input$bins + 1)

        # draw the histogram with the specified number of bins
        hist(x, breaks = bins, col = 'darkgray', border = 'white')
    })
}

Para empezar, server es una función que precisa de al menos dos parámetros: input y output. Luego, dentro de esta función podemos asignar a los contenedores de “outputs” que creamos anteriormente en ui un gráfico o alguna salida en especial. Por ejemplo, usando output$distPlot le estamos diciendo que el objeto que se llama distPlot tiene que hacer algo. En este caso, mostrar un plot (renderPlot()) con un conjunto de códigos en R, a los que no deberían presarle mucha atención en este ejemplo - vamos a usar otros con los que estén más familiarizados en nuestra aplicación. Con todo presten atención que la última linea es un histograma, que es lo que termina mostrándonos en la aplicación.

Finalmente, enfoquémonos un segundo en el uso de input$bins ¿Qué valor lleva? Acá es donde está la magia de Shiny: el objeto bins lo nombramos anteriormente, es el sliderInput. El valor que tiene es justamente el valor seleccionado por el usuario !

Elementos inputs y outputs

En muy resumidas cuentas, entonces, una aplicación de shiny tiene un conjunto de inputs mediante el cual los usuarios pueden comunicarse (esto es, seleccionar, buscar, agrupar, entre otras funciones) y outputs donde se muestran salidas (tablas, gráficos, mapas, texto, entre otros formatos de salida) en base a la información que proveyó el usuario.

Estos “objetos” o “elementos” de nuestras aplicaciones tienen un formato compartido:

Relean con atención el ejemplo que nos da por default RStudio para ver si pueden identificar estos patrones en cómo se definen los objetos.

Nuestra primera aplicación: un comparador de precios

El programa del gobierno argentino Precios Claros lleva más de dos años (estoy escribiendo esto en el 2020). Su objetivo es simple: brindar a los/as consumidores/as la posibilidad de consultar el precio de diversos prductos en un conjunto de comercios cercanos al lugar donde circulan. Como vimos en otro capítulo, es posible scapear los precios para todos los comercios mediante el uso de la API a la cual la página consulta.

Los datos con los que vamos a trabajar consiste en los precios reportados para un día de junio de 2020 y para una muestra de 5 sucursales por provincia de Argentina, según la infromación disponible en la página del programa del gobierno argentino Precios Claros. Los datos están separados en tres objetos distintos y tenemos que hacer algo de data wrangling antes de poder utilizarlos en nuestra aplicación de shiny.

library(tidyverse)
load(url("https://github.com/martinmontane/martinmontane.github.io/raw/master/listaPreciosXSucursal.RData"))
productosXCategoria <- read_delim(file = "https://raw.githubusercontent.com/martinmontane/martinmontane.github.io/master/productosCategoria.csv",delim = ";")
sucursales <- read_delim(file = "https://raw.githubusercontent.com/martinmontane/martinmontane.github.io/master/sucursales.csv",delim = ";")

Tienen que tener tres objetos cargados en la sesión de R. El primero de ellos, listaPreciosXSucursal contiene información sobre los precios para cada una de un conjunto de sucursales. Está en formato de listas - cada posición de esa lista es un data frame - con lo cual lo que vamos a hacer es simplemente pasar esto a un data.frame y hacer alguna limpieza de columnas que no necesitamos:

listaPreciosXSucursal <- listaPreciosXSucursal %>% bind_rows() 
listaPreciosXSucursal <- listaPreciosXSucursal <- listaPreciosXSucursal %>% select(-precioMax,-cantSucursalesDisponible)
listaPreciosXSucursal <- listaPreciosXSucursal %>% rename(precio=precioMin)

Por otro lado, el data.frame productosXCategoria nos indica para cada uno de los productos la categoría a la que pertenece en el sistema de categorías, según Precios Claros. Finalmente, sucursales cuenta con información sobre la ubicación y descripción de las 120 sucursales que fueron seleccionadas para este ejercicio.

Para esta primera aplicación vamos a necesitar agregar información sobre la sucursal donde se vende el producto, por lo cual vamos a hacer un join con el data.frame de sucursales y un poco de limpieza (eliminamos columnas que no queremos mostrar)

listaPreciosXSucursal <- left_join(listaPreciosXSucursal,
                                   sucursales %>% select(id,sucursalTipo,banderaDescripcion,localidad),
                                   by=c("idSucursal"="id")) %>% 
                          select(-presentacion,-idSucursal)

La última modificación que vamos a hacer es quedarnos solo con 10.000 productos de los casi 40.000 que hay en la base. Esto se hace simplemente para hacer más dinámica la nota de clase y perder menos tiempo cargando las aplicaciones, pero se puede adaptar perfectamente para que muestre todos los productos.

set.seed(1)
productos <- listaPreciosXSucursal %>% pull(id) %>% unique()
listaPreciosXSucursal <- listaPreciosXSucursal %>% 
                         filter(id %in% sample(productos,10000)) %>% 
                         select(-id)

Como regla general siempre que desarrollen una aplicación de shiny app hagan todas las transformaciones de datos por fuera del archivo app.R. Esto es así porque es agregar una carga de trabajo adicional a la aplicación cuando no hace falta que se haga allí. Por esta razón es que primero hacemos todo el trabajo de data wrangling antes de iniciar con la aplicación. Lo que nos queda, es guardar de alguna manera estos datos. Suele ser útil hacerlo en archivos .RData, que consiste en “sacarle una foto” a los objetos así como están cargados en la sesión de R. Pruebenlo por ustedes mismos:

save(file = "preciosShiny.RData",listaPreciosXSucursal)

El código de la aplicación

En esta sección voy a volcar el código completo de nuestra aplicación, basándome en el diseño de la aplicación de prueba de RStudio. Consiste solo en cambiar el input y el output con nuestros datos. Primero ejecutémoslo y después veamos en detalle cómo funciona cada elemento. En la animación inmediatamente debajo del código van a ver cómo luce la ejecución del código en un proyecto de RStudio. Rercuerden tener instalados los tres paquetes (shiny, tidyverse y DT) y tener el archivo “preciosShiny.RData” en la carpeta del proyeto.

# Carga de paquetes
library(shiny)
library(tidyverse)
library(DT)
# Carga de datos
load("preciosShiny.RData")
# Definimos los elementos que componen la User Inteface de la aplicación
ui <- fluidPage(

    titlePanel("Buscador de precios"),

    # En este caso, vamos a usar un selectizeInput(), que nos permite hacer una búsqueda
    sidebarLayout(
        sidebarPanel(
            selectizeInput(inputId = "producto", width = "100%",
                                              label = "Elegí un producto",
                                              choices = unique(listaPreciosXSucursal %>% pull(nombre)),
                                              multiple=FALSE,
                                              options = list(placeholder = "Escribí un nombre de producto",
                                                             maxOptions=10))
        ),
        # Vamos a mostrar una tabla. Para eso vamos a usar al paquete DT (Data Table)
        mainPanel(
           DT::dataTableOutput(outputId = "TablaPrecios")
        )
    )
)

# En el server definimos las interacciónes. En este caso, cada vez que input$producto cambie, Se mostrará una tabla en TablaPrecios con los datos sobre el producto seleccionado
server <- function(input, output) {

  output$TablaPrecios <- DT::renderDataTable({
  listaPreciosXSucursal %>% 
      filter(nombre %in% input$producto) 
  })
}

# Damos la orden para que se ejecute la aplicación
shinyApp(ui = ui, server = server)

Ejecutando nuestro proyecto de shiny

La estructura de la aplicación es exactamente igual a la del inicio, la diferencia estuvo en los inputs y outputs que especificamos. Por un lado, reemplazamos el sliderInput por un selectizeInput, que nos permite seleccionar uno o más valores de un vector que le pasemos. Revisemos un poco esa parte del código:

            selectizeInput(inputId = "producto", width = "100%",
                                              label = "Elegí un producto",
                                              choices = unique(listaPreciosXSucursal %>% pull(nombre)),
                                              multiple=FALSE,
                                              options = list(placeholder = "Escribí un nombre de producto",
                                                             maxOptions=10))

Le estamos diciendo que se llamará “producto”, y que el texto - o etiqueta - será “Elegí un producto”. Por otro lado, tenemos que pasarle los valores que queremos dejar elegir, en este caso el vector de nombres de productos. El parámetro “multiple” determina si podemos seleccionar más de un producto o no, mientras que en opciones podemos pasar una lista con distintas especificaciones para nuestro selectizeInput, eneste caso solo mostramos un texto como “placeholder” (cuando no hay nada escrito o seleccionado) y dejamos que solo muestre 10 sugerencias.

Por otro lado, reemplazamos el plotOutput por dataTableOutput, que nos permite mostrar una tabla con ciertas características, tales como poder ordenar o filtrar. el prefijo DT:: simplemente hace referencia a que la función dataTableOutput es del paquete DT. En la parte de DT::renderDataTable() es donde decimos específicamente que queremos mostrar los datos de listaPreciosXSucursal que tienen como nombre el que valor que ha indicado el usuario.

Publicando en Shinyapps.io

Una vez que diseñamos nuestra aplicación ya estamos en condiciones de subirla a internet para que otra persona pueda acceder. Existen diversas maneras de hacerla, pero es importante aclarar que es necesario que exista una computadora - un servidor - que esté esperando la consulta de distintos usuarios. RStudio ofrece un sistema en “nube” que hace todo muy simple, y brinda hasta 25 horas de uso gratuito de la aplicación.

El primer paso para poder publicar la aplicación es crearse una cuenta en este servicio. Vayan a la página oficial de Shiny apps y hagan click en Sing Up. Ingresen un mail - no hace falta ni siquiera que sea real, ya que no pide comprobación de mail - y pueden poner cualquier nombre que esté disponible para que puedan acceder a la aplicación. Por ejemplo, en este caso creé pruebafepp, por lo cual las aplicaciones se podrán acceder dentro de https://pruebafepp.shinyapps.io/NombreDeLaAplicacion.

Registrándose en shinyapps.io

Una vez que hicimos esto, la página de recepción de shinyapps.io nos va a explicar cómo hacerlo. Lo importante es que copíen las tres lineas que están en el paso 2 y ejecutarlo luego de haber instalado el paquete “rsconnect”. Lo que hace ese código es asociar la cuenta de shinyapps.io a nuestro RStudio, y por eso ahora podemos verla listada dentro de “Manage Accounts”.

Al tener asociada la cuenta también podemos publicar nuestra aplicación. Recuerden que debemos estar en el proyecto que tiene al archivo app.R y todos los archivos que usamos en la aplicación dentro de esa carpeta. Publicar es muy fácil: desde el menú de arriba a la derecha eligen “Publish” y solo tienen que poner un nombre para acceder luego a través de internet. Yo le puse de nombre PreciosClarosPrueba, por lo que estará disponible para compartir con cualquier persona en la dirección https://pruebafepp.shinyapps.io/PreciosClarosPrueba/. Pueden consultarla, si no se han consumido las horas mensuales de uso.

Registrando shinyapps.io en Rstudio y publicando la aplicación

Incorporando un nuevo output y actualizando la aplicación

Por último, lo que vamos a hacer es modificar levemente nuestra aplicación para agregar un gráfico de densidad para el producto seleccionado. En el siguiente “chunk” van a ver el código completo, deberían copiarlo para reemplazar en su código “app.R” y volver a publicar la aplicación para que se actualice en internet.

library(shiny)
library(tidyverse)
library(DT)
load("preciosShiny.RData")
# Definimos los elementos que componen la User Inteface de la aplicación
ui <- fluidPage(
    
    titlePanel("Buscador de precios"),
    
    # En este caso, vamos a usar un selectizeInput(), que nos permite hacer una búsqueda
    sidebarLayout(
        sidebarPanel(
            selectizeInput(inputId = "producto", width = "100%",
                           label = "Elegí un producto",
                           choices = unique(listaPreciosXSucursal %>% pull(nombre)),
                           multiple=FALSE,
                           options = list(placeholder = "Escribí un nombre de producto",
                                          maxOptions=10))
        ),
        # Vamos a mostrar una tabla. Para eso vamos a usar al paquete DT (Data Table)
        mainPanel(
            DT::dataTableOutput(outputId = "TablaPrecios"),
            plotOutput(outputId="GraficoPrecios")
        )
    )
)

# En el server definimos las interacciónes. En este caso, cada vez que input$producto cambie, Se mostrará una tabla en TablaPrecios con los datos sobre el producto seleccionado
server <- function(input, output) {
    
    output$TablaPrecios <- DT::renderDataTable({
        listaPreciosXSucursal %>% 
            filter(nombre %in% input$producto) 
    })
    
      output$GraficoPrecios <- renderPlot({
    ggplot(listaPreciosXSucursal %>%
             filter(nombre %in% input$producto)) +
      geom_density(aes(x=precio)) +
      theme_minimal() +
      theme(text=element_text(size=20), axis.text.y = element_blank()) +
      labs(x="",y="",title="Distribución de precios del producto seleccionado")
  })
}

# Damos la orden para que se ejecute la aplicación
shinyApp(ui = ui, server = server)

Si prestan atención, lo único que cambiamos es que incorporamos un plotOutput y un renderPlot para poder agregar nuestro gráfico. Pueden verlo publicado en https://pruebafepp.shinyapps.io/ShinyAppV2/

Ejercicios y extensiones

En https://shiny.rstudio.com/tutorial/ pueden ver un tutorial muy extenso sobre cómo trabajar con Shiny. Recomiendo hacer todo el tutorial, pero en particular la parte de reactive() (sección 17) ¿Cómo podrían readaptar la última aplicación que desarrollamos usando esta función?

Por otro lado, acá pueden revisar la cheatsheet de RStudio